package afcp.alumni.service.impl;

import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import javax.inject.Inject;

import net.sf.jsr107cache.CacheException;
import net.sourceforge.stripes.action.Message;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;

import afcp.alumni.action.base.BaseAction;
import afcp.alumni.action.base.MyActionBeanContext;
import afcp.alumni.dao.FileColumnDao;
import afcp.alumni.dao.MemberDao;
import afcp.alumni.dao.base.Dao.KindInfo;
import afcp.alumni.dao.base.Dao.MemberProcessor;
import afcp.alumni.email.PasswordEmail;
import afcp.alumni.model.Pb;
import afcp.alumni.model.SearchResult;
import afcp.alumni.model.db.FileColumn;
import afcp.alumni.model.db.Member;
import afcp.alumni.model.db.Member.Role;
import afcp.alumni.model.db.Member.Sex;
import afcp.alumni.model.db.MemberUtils;
import afcp.alumni.service.MemberService;
import afcp.alumni.util.Conf;
import afcp.alumni.util.Util;

import com.google.apphosting.api.ApiProxy.OverQuotaException;

/**
 * The Service class is called by the stripes action beans. It implements more
 * complex treatments of this application that souldn't remain in the action.
 * Only services should instantiate the DAOs and call its methods.
 * 
 * @author Antoine
 * 
 */
// TODO changer en service pour gérer les transactions ?
// http://docs.spring.io/spring/docs/2.5.x/reference/transaction.html#transaction-declarative
@Component
public class MemberServiceImpl implements MemberService {
	
	MemberServiceImpl() {}

	private static final Log logger = LogFactory.getLog(MemberServiceImpl.class);

	@Inject
	private MemberDao memberDao;
	// = MemberDaoImpl.getDao()
//	@Inject
//	private RoleDaoImpl roleDao;
//	private RoleDaoImpl roleDao = RoleDaoImpl.getDao();
	@Inject
	private FileColumnDao fileDao;//TODO
	@Inject
	private LoginService loginService;
//	private MailService mailService = SF.mail();

//	/**
//	 * Returns a list of all the members registered in the application.
//	 * 
//	 * @return A list of all the members
//	 */
//	// TODO remplacer par une récupération par vagues avec les curseurs
//	@Override
//	public List<Member> getAllMembers() {
//		return memberDao.findAll();
//	}
	/**
	 * 
	 * @param page page number, from 1 to the number of pages
	 * @return
	 */
	@Override
	public SearchResult<Member> getMembersOfPage(int page, int perPage) {
		return memberDao.findOnePage(page, perPage);
	}
	
//	public SearchResult<Member> getNextMembers(int page, int perPage, Cursor previousCursor) {
//		return memberDao.findWithCursor(page, perPage, previousCursor);
//	}

	
	/**
	 * Must be called before writing a member in database (add or update).
	 * @param member
	 */
	private void initMember(Member member) {
//		if (member.getLogin().getEntity() == null) {
//			member.getLogin().setEntity(member);
//		}
		if (member.noRole()) {
			member.setRole(Role.MEMBER);
		}
	}
	
	private boolean indexMember(Member member) {
		return !member.getRoles().contains(Role.ADMIN);
	}

	/**
	 * Adds a new member in the application or update it if it already exists in
	 * database. Consider to use {@link #memberExists(Member)} to check if it
	 * exists, or {@link #addMember(Member)} to do both in once.
	 * 
	 * @param member
	 *            The new member to add
	 * @return True if successfully added, false otherwise.
	 */
	@Override
	public boolean addOrUpdateMember(Member member) {
		initMember(member);
		boolean success = memberDao.addOrUpdate(member, indexMember(member));
		if (!success) {
			member.addError(Pb.PROBLEM_WITH_DATABASE);
		}
		return success;
	}
	
	@Override
	public boolean addOrUpdateMembers(List<Member> members) {
		for (Member member : members) {
			initMember(member);
		}
		return memberDao.addOrUpdate(members);
	}
	
	/**
	 * add the member if not already in database. otherwise do nothing.
	 * 
	 * @param member
	 * @return <code>true</code> if successfully added, <code>false</code> if
	 *         already exists or if a problem occurred.
	 */
	@Override
	public boolean addMember(Member member, boolean indexes) {
		initMember(member);
		if (memberExists(member)) {
			logger.warn("The member "
					+ member.getFirstName() + " "
					+ member.getLastName() + " (id: "
					+ member.getId()
					+ ") already exists in datastore.");
			member.addError(Pb.ALREADY_IN_DATABASE);
			return false;
		}
		boolean success = memberDao.addOrUpdate(member, indexes);
		if (!success) {
			member.addError(Pb.PROBLEM_WITH_DATABASE);
		}
		return success;
	}
	@Override
	public boolean addMember(Member member) {
		return addMember(member, indexMember(member));
	}
	
	@Override
	public boolean updateMember(Member member) {
		initMember(member);
		if (!memberExists(member)) {
			member.addError(Pb.NOT_IN_DATABASE);
			return false;
		}
		boolean success = memberDao.update(member);
		if (!success) {
			member.addError(Pb.PROBLEM_WITH_DATABASE);
		}
		return success;
	}
	
	
	@Override
	public Member getMember(String login)
	{
		return memberDao.find(login);
	}
	
//	@Override
//	public Member getMemberFromStringKey(String id)
//	{
//		return memberDao.findFromKeyString(id);
//	}

	@Override
	public boolean memberExists(Member member) {
		initMember(member);
		return memberDao.exists(member);
	}

	/**
	 * Removes the member with the given id from the application.
	 * 
	 * @param id
	 *            The id of the member to remove.
	 * @return True if successfully removed, false otherwise.
	 */
	@Override
	public Member removeMember(String id) {
		return memberDao.remove(id);
	}
	
	@Override
	public boolean removeAllMembers(MyActionBeanContext context) {
		loginService.logout(context);
		boolean result = memberDao.removeAll();
//		clearLocalEntities();
		return result;
	}
	
	// TODO nécessaire ? cf problème de création des membres init juste après
	// leur suppression (suppr tous les membres -> déco -> reco -> crée
	// l'administrateur qui vient d'être supprimé).
//	private void clearLocalEntities() {
//		this.roleAdmin = null;
//		this.roleMember = null;
//	}

	@Override
	public SearchResult<Member> searchMembers(String queryString, int page, int perPage) {
		return memberDao.search(queryString, page, perPage);
	}
	
//	public boolean resetMembers() {
//		Member admin = newMemberAdmin();
//		boolean ok = memberDao.removeAll();
//		ok &= memberDao.addOrUpdate(admin);
//		return ok;
//	}
	
	@Override
	public Member initDefaultMembers() {
		return initDefaultMembers(true);
	}
	
	// TODO une erreur se produit autour de cette fonction lorsqu'on vient de
	// supprimer tous les membres et qu'on se reconnecte en tant qu'admin.
	@Override
	public Member initDefaultMembers(boolean isAdmin) {
		Member admin = newMemberAdmin();
		boolean ok = memberDao.addOrUpdate(admin);
//		ok &= getRoleMember() != null;

		// Initialize by also creating the test Member.
		// TODO à supprimer en production
		Member testMember = newMemberTest();
		memberDao.addOrUpdate(testMember);
		
		// ADMIN and MEMBER roles are initialized.
		
		if (ok) {
			return isAdmin ? admin : testMember;
		} else {
			return null;
		}
	}
	
	public Member newMemberAdmin() {
		Member admin = new Member();
		
		admin.setEmailSent(true);
		admin.setPasswordChanged(true);
		admin.setPassword("password");
		admin.setDeletable(false);
		admin.setRole(Role.ADMIN);
		
		admin.setLastName("Administrateur");
		admin.setFirstName("Administrateur");
		admin.setSex(Sex.MALE);
		admin.setChineseName("你好");
		Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CET"), Locale.FRANCE);
		calendar.set(2000, 1, 1);
		admin.setBirthDate(calendar.getTime());
		admin.setParisTechEntranceYear(2013);
		admin.setParisTechSchool("Bureau AFCP");
		admin.setChineseUniversity("Bureau AFCP");
		admin.setScholarship("Bourse AFCP");
		admin.setDomainInFrance("Administration");
		admin.setDomainInChina("Administration");
		admin.setOtherDiploma("Procrastinator");
		admin.setDiplomaUniversity("Procraschool");
		admin.setFirstJobCountry("France");
		admin.setInternship("Bureau AFCP");
		admin.setFirstJob("Administrateur");
		admin.setJobCountry("France");
		admin.setEmployer("AFCP");
		admin.setEmail("admin");
		admin.setPhone("0123456789");
		admin.setAddressSupplement("Chambre 42");
		admin.setAddress("1 rue de l'administration");
		admin.setCountry("France");
		admin.setWeibo("weibo");
		admin.setWeixin("weixin");
		admin.setNotes("Le plus important du site");
		
		return admin;
	}
	
	public Member newMemberTest() {
		Member test = new Member();
		test.setEmailSent(true);
		test.setPasswordChanged(false);
		test.setPassword(TEST_LOGIN);
		test.setDeletable(true);
		test.setRole(Role.MEMBER);
		
		test.setLastName("Du Test");
		test.setFirstName("Robert");
		test.setSex(Sex.FEMALE);
		test.setChineseName("很漂亮");
		Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("CET"), Locale.FRANCE);
		calendar.set(1990, 2, 3);
		test.setBirthDate(calendar.getTime());
		test.setParisTechEntranceYear(2012);
		test.setParisTechSchool("Test ParisTech");
		test.setChineseUniversity("大Test学院");
		test.setScholarship("Bourse AFCP");
		test.setDomainInFrance("Test");
		test.setDomainInChina("Test");
		test.setOtherDiploma("Procrastinator");
		test.setDiplomaUniversity("Procraschool");
		test.setFirstJobCountry("France");
		test.setInternship("Salon AFCP");
		test.setFirstJob("Testeur");
		test.setJobCountry("France");
		test.setEmployer("Bureau AFCP");
		test.setEmail("test");
		test.setPhone("0123456789");
		test.setAddress("13 rue du test hanover");
		test.setCountry("France");
		test.setWeibo("weibo");
		test.setWeixin("weixin");
		test.setNotes("Quelconque");
		
		return test;
	}

	// Roles

//	@Override
//	public Role getRole(String id) {
//		return roleDao.find(id);
//	}
//	
//	@Override
//	public List<Role> getAllRoles() {
//		return roleDao.findAll();
//	}
//	@Override
//	public SearchResult<Role> getRolesOfPage(int page, int perPage) {
//		return roleDao.findOnePage(page, perPage);
//	}
//	
//	private Role newRoleAdmin() {
//		Role roleAdmin = new Role();
//		roleAdmin.setDeletable(false);
//		roleAdmin.setId(Role.ROLE_ADMIN);
//		roleAdmin.setLabel(Role.ROLE_ADMIN_LABEL);
//		return roleAdmin;
//	}
//	
//	public void clearRoles() {
//		roleAdmin = null;
//		roleMember = null;
//	}
//
//	// TODO les variables stockées comme ça semblent poser problème lorsque tous
//	// les membres sont supprimées. Les entités sont supprimées de la base, et
//	// les objets ne sont plus utilisables !
//	private Role roleAdmin = null;
//	@Override
//	public Role getRoleAdmin() {
//		if (roleAdmin == null) {
//			roleAdmin = roleDao.find(Role.ROLE_ADMIN);
//			if (roleAdmin == null) {
//				roleAdmin = newRoleAdmin();
//				roleDao.addOrUpdate(roleAdmin);
//			}
//		}
//		return roleAdmin;
//	}
//	
//	private Role newRoleMember() {
//		Role roleMember = new Role();
//		roleMember.setDeletable(true);
//		roleMember.setId(Role.ROLE_MEMBER);
//		roleMember.setLabel(Role.ROLE_MEMBER_LABEL);
//		return roleMember;
//	}
//	private Role roleMember = null;
//	@Override
//	public Role getRoleMember() {
//		if (roleMember == null) {
//			roleMember = roleDao.find(Role.ROLE_MEMBER);
//			if (roleMember == null) {
//				roleMember = newRoleMember();
//				roleDao.addOrUpdate(roleMember);
//			}
//		}
//		return roleMember;
//	}
//
//	/**
//	 * Don't check if the role already exists in database. Use
//	 * {@link #roleExists(Role)} to check if it exists, or
//	 * {@link #addRole(Role)} to do both in once.
//	 * 
//	 * @param role
//	 * @return
//	 */
//	@Override
//	public boolean addOrUpdateRole(Role role) {
//		return roleDao.addOrUpdate(role);
//	}
//	
//	/**
//	 * Check if the role exists, and if not, add it in database. If found in
//	 * database, do nothing and return false.
//	 * 
//	 * @param role
//	 *            The role to add in database if it does not already exists.
//	 * @return true if successfully added, false if it already exists in
//	 *         database or if a problem occurred when trying to add the role in
//	 *         database.
//	 */
//	@Override
//	public boolean addRole(Role role) {
//		if (roleExists(role)) {
//			return false;
//		}
//		return roleDao.addOrUpdate(role);
//	}
//
//	@Override
//	public boolean roleExists(Role role) {
//		return roleDao.exists(role);
//	}
//
//	@Override
//	public boolean roleExists(Member member) {
//		Role role = roleDao.find(member.getRoles().get(0).getId());
//		boolean result = role != null;
//		if (result) {
//			member.getRoles().set(0, role);
//		}
//		return result;
//	}
//	
//	@Override
//	public Role removeRole(String id) {
//		return roleDao.remove(id);
//	}
//
//	@Override
//	public boolean resetRoles() {
//		List<Role> oldRoles = roleDao.findAll();
//		List<Role> rolesToKeep = new LinkedList<>();
//
//		for (Role role : oldRoles) {
//			if (!role.isDeletable() || Role.ROLE_ADMIN.equals(role.getId())) {
//				rolesToKeep.add(new Role(role));
//				// Or delete from a list of roles to delete, and call
//				// roleDao.remove(List) instead of removeAll().
//			}
//		}
//		if (rolesToKeep.isEmpty()) {
//			rolesToKeep.add(newRoleAdmin());
//			logger.info("No role to keep, creating admin role.");
//		}
//
////		boolean ok = roleDao.remove(oldRoles);
//		boolean ok = roleDao.removeAll();
//		if (ok) {
//			logger.info("Adds the new roles");
//			ok &= roleDao.addOrUpdate(rolesToKeep);
//		} else {
//			logger.info("A problem occurred while deleting the roles. Won't add the new roles.");
//		}
//		return ok;
//	}
//	
//	@Override
//	public boolean removeRoles() {
//		return roleDao.removeAll();
//	}
//	
//	@Override
//	public boolean addDefaultRoles() {
//		List<Role> defaultRoles = new LinkedList<>();
//		defaultRoles.add(newRoleAdmin());
//		defaultRoles.add(newRoleMember());
//		return roleDao.addOrUpdate(defaultRoles);
//	}
	
	@Override
	public boolean addToIndex(List<Member> members) {
		return memberDao.addToIndex(members);
	}

	// TODO tester, vérif que la nouvelle implémentation fonctionne.
	@Override
	public boolean rebuildIndex() {
		boolean success = memberDao.clearIndex();
		
		success &= memberDao.processAllMembers(new MemberProcessor<Member>() {
			@Override
			public boolean process(List<Member> membersOfOnePage) {
				return memberDao.addToIndex(membersOfOnePage);
			}
		});

//		int perPage = Conf.BUFFER_SIZE;
//		SearchResult<Member> result;
//		int p = 1;
//		do {
//			result = memberDao.findOnePage(p++, perPage);
//			if (!result.getList().isEmpty()) {
//				success &= memberDao.addToIndex(result.getList());
//			}
//		} while (result.getHasNextPage());

		return success;
	}
	
//	private List<FileColumn> columns = null;
	@Override
	public List<FileColumn> getAllFileColumns() {
		List<FileColumn> columns = fileDao.findAll();
		List<FileColumn> defaultColumns = MemberUtils.defaultColumns();
		boolean changed = completeWithMissingColumns(columns, defaultColumns);
		changed |= removeExtraColumns(columns, defaultColumns);
		if (changed) {
			logger.info("No Column in database. Initialize column list with default columns");
//			columns = MemberUtils.defaultColumns();
			fileDao.addOrUpdate(columns);
		}
		return columns;
	}
	
	boolean completeWithMissingColumns(List<FileColumn> columns, List<FileColumn> defaultColumns) {
		boolean changed = false;
		for (FileColumn column : defaultColumns) {
			if (!columns.contains(column)) {
				columns.add(column);
				if (!changed) {
					changed = true;
				}
			}
		}
		return changed;
	}
	
	boolean removeExtraColumns(List<FileColumn> columns, List<FileColumn> defaultColumns) {
		boolean changed = false;
		int i = 0;
		for (FileColumn column : columns) {
			if (!defaultColumns.contains(column)) {
				columns.remove(i);
				if (!changed) {
					changed = true;
				}
			}
			i++;
		}
		return changed;
	}
	
	@Override
	public boolean resetColumns() {
		return fileDao.addOrUpdate(MemberUtils.defaultColumns());
	}
	
	@Override
	public boolean updateColumns(List<FileColumn> columns) {
		return fileDao.addOrUpdate(columns, true);
	}
	
	@Override
	public List<Message> generatePassword(String memberId, Locale locale) {
		Member member = getMember(memberId);
		return generatePassword(member, locale);
	}
	
	private List<Message> generatePassword(Member member, Locale locale) {
		List<Message> failures = new LinkedList<>();
		if (member.isEmailSent()) {
			logger.info("No password will be generated for member "
					+ member.getFirstName() + " " + member.getLastName()
					+ "(id: " + member.getId()
					+ ") nor email sent: password already generated and sent.");
//			failures.add(BaseAction.pbToMessage(Pb.PASSWORD_ALREADY_GENERATED,
//					member.getFirstName(), member.getLastName(), member.getId()));
		} else if (member.getEmail() == null || member.getEmail().isEmpty()) {
			failures.add(BaseAction.pbToMessage(Pb.NO_EMAIL_CANT_SEND,
					member.getFirstName(), member.getLastName(), member.getId()));
		} else {
			String newPassword = Util.generatePassword();

			// Try to send an email with the generated password.
			try {
				new PasswordEmail(member, newPassword, locale).send();
				member.setEmailSent(true);
				member.setPassword(newPassword);
				boolean writtenInDatabase = memberDao
						.updateNoReindexingNoValidation(member);
				if (!writtenInDatabase) {
					logger.error("Could not write new member password in database. Member: "
							+ member.getId());
					failures.add(BaseAction.pbToMessage(
							Pb.PROBLEM_WITH_DATABASE, member.getFirstName(),
							member.getLastName(), member.getId()));
				}
			} catch (OverQuotaException e) {
				logger.warn("Could not send email with generated password. No email quota left for today.");
				// logger.warn(e.getMessage());
				failures.add(BaseAction.pbToMessage(Pb.NO_EMAIL_QUOTA,
						member.getFirstName(), member.getLastName(),
						member.getId()));
			} catch (Exception e) {
				logger.error("Could not send email with generated password. "
						+ "An error occurred when sending the email.", e);
				failures.add(BaseAction.pbToMessage(Pb.PROBLEM_EMAIL_SENDING,
						member.getFirstName(), member.getLastName(),
						member.getId()));
			}
		}

		return failures;
	}
	
	@Override
	public List<Message> generateAllPasswords(Locale locale) {
		List<Message> failures = new LinkedList<>();
		int perPage = Conf.BUFFER_SIZE;
//		Cursor cursor = null;
		SearchResult<Member> result;
		int p = 1;
		do {
//			result = memberDao.findWithCursor(p++, perPage, cursor);
			result = memberDao.findOnePage(p++, perPage);
//			cursor = result.getCursor();
			if (!result.getList().isEmpty()) {
				for (Member member : result.getList()) {
					failures.addAll(generatePassword(member, locale));
				}
			}
//		} while (cursor != null);
		} while (result.getHasNextPage());
		
		return failures;
	}
	
	public void clearSessionsAndCache() throws CacheException {
		memberDao.clearSessions();
		memberDao.clearCache();
	}
	
	public List<KindInfo> findKindsWithStats() {
		return memberDao.listKindsWithStats();
	}
	
	public List<KindInfo> findKinds() {
		return memberDao.listKinds();
	}
	
	public boolean removeByKindName(String kindName, MyActionBeanContext context) {
		if (Member.class.getSimpleName().equals(kindName)) {
			loginService.logout(context);
		}
		
		return memberDao.removeByKindName(kindName);
	}

	public boolean clearAllDatastore(MyActionBeanContext context) {
		return memberDao.clearAllDatastore(context);
	}
	
//	/**
//	 * @see MemberDaoImpl#newTransactionObject()
//	 */
//	@Override
//	public Transaction newTransactionObject() {
//		return memberDao.newTransactionObject();
//	}
//
//	@Override
//	public void beginTransaction(Transaction txn) {
//		memberDao.beginTransaction(txn);
//	}
//
//	@Override
//	public void commitTransaction(Transaction txn) {
//		memberDao.commitTransaction(txn);
//	}
//
//	@Override
//	public void rollbackTransaction(Transaction txn) {
//		memberDao.rollbackTransaction(txn);
//	}

}
